#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
资源混淆器
处理图片、资源文件的混淆
"""

import os
import json
import random
import shutil
import re
from typing import Dict, List, Optional, Set, Tuple
from pathlib import Path
from ..builtin_config_module.builtin_config import BuiltInConfig
from ..utils_module.logger import ObfuscationLogger
from ..utils_module.language_utils import SUPPORTED_LANGUAGES, collect_extensions, get_extension_group

class ResourceObfuscator:
    """资源混淆器"""

    def __init__(self, config: BuiltInConfig, logger: ObfuscationLogger):
        self.config = config
        self.logger = logger
        self._renamed_files: Dict[str, str] = {}

        # 支持的资源文件扩展名
        self.supported_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.svg']

        # 资源引用模式（针对不同语言和文件类型）
        self.reference_patterns = {
            # iOS 资源引用模式
            'swift': [
                (r'UIImage\s*\(\s*named:\s*"([^"]+)"\s*\)', 'UIImage(named: "{}")'),
                (r'UIImage\s*\(\s*systemName:\s*"([^"]+)"\s*\)', None),  # 系统图标不替换
                (r'#imageLiteral\s*\(\s*resourceName:\s*"([^"]+)"\s*\)', '#imageLiteral(resourceName: "{}")'),
            ],
            'objc': [
                (r'\[UIImage\s+imageNamed:\s*@"([^"]+)"\]', '[UIImage imageNamed:@"{}"]'),
                (r'imageNamed:\s*@"([^"]+)"', 'imageNamed:@"{}"'),
            ],
            # Android 资源引用模式
            'java': [
                (r'R\.drawable\.(\w+)', None),  # Android R资源需要特殊处理
                (r'@drawable/(\w+)', '@drawable/{}'),
            ],
            'kotlin': [
                (r'R\.drawable\.(\w+)', None),  # Android R资源需要特殊处理
                (r'@drawable/(\w+)', '@drawable/{}'),
            ],
            # XML 资源引用
            'xml': [
                (r'android:src="@drawable/([^"]+)"', 'android:src="@drawable/{}"'),
                (r'app:srcCompat="@drawable/([^"]+)"', 'app:srcCompat="@drawable/{}"'),
            ],
            # Storyboard/XIB 引用
            'storyboard': [
                (r'image="([^"]+)"', 'image="{}"'),
                (r'imageName="([^"]+)"', 'imageName="{}"'),
            ],
            # HTML/CSS 引用
            'web': [
                (r'src\s*=\s*["\']([^"\']+\.(?:png|jpg|jpeg|gif|svg))["\']', 'src="{}"'),
                (r'url\s*\(\s*["\']?([^"\']+\.(?:png|jpg|jpeg|gif|svg))["\']?\s*\)', 'url("{}")'),
            ]
        }

    def obfuscate_resources(self):
        """混淆资源文件"""
        features = self.config.advanced_features
        if not features.resource_obfuscation_enabled:
            return

        self.logger.log_operation("开始资源混淆", "混淆图片和资源文件")

        # 查找资源文件
        resource_files = self._find_resource_files()

        for file_path in resource_files:
            try:
                if features.modify_md5:
                    self._modify_file_md5(file_path)

                if features.rename_resources:
                    self._rename_resource_file(file_path)

                self.logger.log_file_processing(file_path, "已混淆资源")

            except Exception as e:
                self.logger.log_error(f"资源混淆失败: {file_path}, 错误: {str(e)}")

    def _find_resource_files(self) -> List[str]:
        """查找资源文件"""
        resource_files = []
        project_path = Path(self.config.project_path)

        for file_path in project_path.rglob('*'):
            if file_path.is_file() and file_path.suffix.lower() in self.supported_extensions:
                # 检查排除目录
                if not any(exclude_dir in file_path.parts for exclude_dir in self.config.file_filters.exclude_directories):
                    resource_files.append(str(file_path))

        return resource_files

    def _modify_file_md5(self, file_path: str):
        """修改文件MD5值，确保不会破坏资源格式"""
        suffix = Path(file_path).suffix.lower()

        if suffix == '.png':
            self._inject_png_text_chunk(file_path)
            return

        if suffix == '.svg':
            self._inject_svg_comment(file_path)
            return

        self.logger.log_warning(
            f"跳过MD5修改: {file_path} - 暂不支持安全的 {suffix} 变更"
        )

    def _inject_png_text_chunk(self, file_path: str):
        """在PNG文件中注入安全的文本块以扰动MD5"""
        import struct
        import zlib

        try:
            with open(file_path, 'rb') as handle:
                data = handle.read()

            signature = b'\x89PNG\r\n\x1a\n'
            if not data.startswith(signature):
                self.logger.log_warning(f"PNG签名不匹配，跳过: {file_path}")
                return

            iend_marker = b'\x00\x00\x00\x00IEND\xaeB`\x82'
            iend_index = data.rfind(iend_marker)
            if iend_index == -1:
                self.logger.log_warning(f"未找到IEND块，跳过: {file_path}")
                return

            random_key = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=6))
            random_value = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
            payload = f"meta-{random_key}\x00{random_value}".encode('utf-8')

            chunk_type = b'tEXt'
            chunk_length = struct.pack('>I', len(payload))
            crc = struct.pack('>I', zlib.crc32(chunk_type + payload) & 0xffffffff)
            text_chunk = chunk_length + chunk_type + payload + crc

            modified = data[:iend_index] + text_chunk + data[iend_index:]

            if not self.config.dry_run:
                with open(file_path, 'wb') as handle:
                    handle.write(modified)

            self.logger.log_operation("修改MD5", f"注入PNG文本块: {file_path}")

        except Exception as exc:
            self.logger.log_error(f"PNG MD5 修改失败 {file_path}: {exc}")

    def _inject_svg_comment(self, file_path: str):
        """在SVG文件中插入注释以扰动MD5"""
        try:
            content = Path(file_path).read_text(encoding='utf-8')
        except Exception as exc:
            self.logger.log_error(f"读取SVG失败 {file_path}: {exc}")
            return

        comment = f"<!-- obf:{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=12))} -->"

        insertion_index = content.find('<svg')
        if insertion_index == -1:
            self.logger.log_warning(f"无法识别SVG结构，跳过: {file_path}")
            return

        closing = content.find('>', insertion_index)
        if closing == -1:
            self.logger.log_warning(f"无法定位SVG标签，跳过: {file_path}")
            return

        modified = content[:closing + 1] + '\n' + comment + content[closing + 1:]

        if not self.config.dry_run:
            Path(file_path).write_text(modified, encoding='utf-8')

        self.logger.log_operation("修改MD5", f"插入SVG注释: {file_path}")

    def _rename_resource_file(self, file_path: str):
        """重命名资源文件"""
        try:
            file_path_obj = Path(file_path)
            parent_dir = file_path_obj.parent
            extension = file_path_obj.suffix

            # 生成新的文件名
            new_name = self._generate_random_filename(extension)
            new_path = parent_dir / new_name

            # 重命名文件
            if not self.config.dry_run:
                shutil.move(file_path, new_path)
                self._renamed_files[str(file_path_obj)] = str(new_path)
                # 使用改进的安全引用更新方法
                self._update_resource_references_safe(file_path_obj.name, new_name)
                self._update_ios_asset_catalog(file_path_obj, Path(new_path))
            else:
                self.logger.log_warning("Dry run 模式下跳过资源重命名引用更新")

            self.logger.log_operation("重命名资源", f"{file_path} -> {new_path}")

        except Exception as e:
            self.logger.log_error(f"重命名资源失败: {file_path}, 错误: {str(e)}")

    def _generate_random_filename(self, extension: str) -> str:
        """生成随机文件名"""
        # 使用随机字符串作为文件名
        random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16))
        return f"{random_string}{extension}"

    def restore_resources(self):
        """恢复资源文件"""
        self.logger.log_operation("开始恢复资源", "恢复资源文件到原始状态")

        for original, renamed in self._renamed_files.items():
            try:
                if os.path.exists(renamed) and not self.config.dry_run:
                    shutil.move(renamed, original)
                    self.logger.log_operation("恢复资源", f"{renamed} -> {original}")
            except Exception as exc:
                self.logger.log_warning(f"恢复资源失败 {renamed}: {exc}")

    def _update_resource_references_safe(self, original_name: str, new_name: str):
        """
        安全地更新项目内对资源文件的引用

        使用模式匹配而不是简单的字符串替换，避免误替换
        """
        language_extensions = collect_extensions(SUPPORTED_LANGUAGES)
        text_extensions = language_extensions.union(set(get_extension_group('ios_resource_text')))
        project_path = Path(self.config.project_path)

        # 统计替换情况
        total_replacements = 0
        files_updated = []

        for path in project_path.rglob('*'):
            if not path.is_file():
                continue
            if path.suffix.lower() not in text_extensions:
                continue
            if path.name == new_name:
                continue

            try:
                content = path.read_text(encoding='utf-8')
            except (UnicodeDecodeError, OSError):
                continue

            # 检测文件语言类型
            file_language = self._detect_file_language(path)
            if not file_language:
                continue

            # 获取对应的引用模式
            patterns = self._get_patterns_for_language(file_language)
            if not patterns:
                continue

            # 执行安全替换
            updated_content, count = self._safe_replace_references(
                content,
                original_name,
                new_name,
                patterns
            )

            if count > 0 and not self.config.dry_run:
                path.write_text(updated_content, encoding='utf-8')
                self.logger.log_file_processing(
                    str(path),
                    f"更新资源引用 ({count}处)"
                )
                total_replacements += count
                files_updated.append(str(path))

        if total_replacements > 0:
            self.logger.log_operation(
                "资源引用更新",
                f"共更新 {total_replacements} 处引用，涉及 {len(files_updated)} 个文件"
            )

    def _detect_file_language(self, file_path: Path) -> Optional[str]:
        """检测文件的语言类型"""
        extension = file_path.suffix.lower()

        # 扩展名映射到语言
        ext_to_lang = {
            '.swift': 'swift',
            '.m': 'objc',
            '.mm': 'objc',
            '.h': 'objc',
            '.java': 'java',
            '.kt': 'kotlin',
            '.xml': 'xml',
            '.storyboard': 'storyboard',
            '.xib': 'storyboard',
            '.html': 'web',
            '.htm': 'web',
            '.css': 'web',
            '.js': 'web',
            '.ts': 'web',
            '.tsx': 'web',
            '.jsx': 'web',
        }

        return ext_to_lang.get(extension)

    def _get_patterns_for_language(self, language: str) -> List[Tuple[str, Optional[str]]]:
        """获取特定语言的引用模式"""
        return self.reference_patterns.get(language, [])

    def _safe_replace_references(
        self,
        content: str,
        original_name: str,
        new_name: str,
        patterns: List[Tuple[str, Optional[str]]]
    ) -> Tuple[str, int]:
        """
        安全地替换资源引用

        Args:
            content: 文件内容
            original_name: 原始资源名（可能包含扩展名）
            new_name: 新资源名
            patterns: 引用模式列表

        Returns:
            (更新后的内容, 替换次数)
        """
        updated_content = content
        total_replacements = 0

        # 分离文件名和扩展名
        original_base = Path(original_name).stem
        original_ext = Path(original_name).suffix
        new_base = Path(new_name).stem
        new_ext = Path(new_name).suffix

        for pattern_str, replacement_template in patterns:
            if replacement_template is None:
                # 跳过不需要替换的模式（如系统资源）
                continue

            # 为资源名创建更精确的模式
            # 尝试匹配完整文件名或只是基础名
            for orig, new in [
                (original_name, new_name),  # 完整文件名
                (original_base, new_base),  # 仅基础名（无扩展名）
            ]:
                # 转义原始名称中的特殊字符
                escaped_orig = re.escape(orig)

                # 构建完整的搜索模式
                search_pattern = pattern_str.replace('([^"]+)', escaped_orig)
                search_pattern = search_pattern.replace('(\w+)', escaped_orig)

                # 执行替换
                matches = list(re.finditer(search_pattern, updated_content))
                if matches:
                    # 从后向前替换，避免位置偏移
                    for match in reversed(matches):
                        # 构建替换字符串
                        replacement = replacement_template.format(new)

                        # 执行替换
                        start = match.start()
                        end = match.end()
                        updated_content = (
                            updated_content[:start] +
                            replacement +
                            updated_content[end:]
                        )
                        total_replacements += 1

                    # 记录替换信息
                    self.logger.log_operation(
                        "资源引用替换",
                        f"模式 '{pattern_str}' 替换了 {len(matches)} 处"
                    )

        return updated_content, total_replacements

    def _update_ios_asset_catalog(self, original_path: Path, new_path: Path):
        """当资源文件位于xcassets中时更新Contents.json引用"""
        asset_dir = original_path.parent
        json_path = None

        # 寻找最近的 *.imageset 或 *.appiconset 目录
        for _ in range(3):  # 通常结构较浅
            if asset_dir is None:
                break
            if asset_dir.name.endswith(('.imageset', '.appiconset', '.symbolset')):
                candidate = asset_dir / 'Contents.json'
                if candidate.exists():
                    json_path = candidate
                break
            asset_dir = asset_dir.parent

        if not json_path or self.config.dry_run:
            return

        try:
            with open(json_path, 'r', encoding='utf-8') as handle:
                data = json.load(handle)

            modified = False
            old_file = original_path.name
            new_file = new_path.name

            for image_entry in data.get('images', []):
                filename = image_entry.get('filename')
                if filename == old_file:
                    image_entry['filename'] = new_file
                    modified = True

            if modified:
                with open(json_path, 'w', encoding='utf-8') as handle:
                    json.dump(data, handle, ensure_ascii=False, indent=2)
                self.logger.log_file_processing(str(json_path), "更新Contents.json引用")

        except Exception as exc:
            self.logger.log_warning(f"更新Contents.json失败 {json_path}: {exc}")